/**
 * @file
 *
 * @ingroup mil1986be9x_libi2c
 *
 * @brief LibI2C bus driver for the Synchronous Serial Port (SSP).
 */

/*
 * Copyright (c) 2008
 * Embedded Brains GmbH
 * Obere Lagerstr. 30
 * D-82178 Puchheim
 * Germany
 * rtems@embedded-brains.de
 *
 * The license and distribution terms for this file may be found in the file
 * LICENSE in this distribution or at http://www.rtems.com/license/LICENSE.
 */

#include <stdbool.h>

#include <bsp/ssp.h>
#include <bsp/1986be9x.h>
#include <bsp/irq.h>
#include <bsp/system-clocks.h>
#include <bsp/io.h>

#define RTEMS_STATUS_CHECKS_USE_PRINTK

#include <rtems/status-checks.h>

#define MIL1986BE9X_SSP_NUMBER 2

#define MIL1986BE9X_SSP_FIFO_SIZE 8

#define MIL1986BE9X_SSP_BAUD_RATE 2000000

typedef struct {
  uint32_t cr0;
  uint32_t cr1;
  uint32_t dr;
  uint32_t sr;
  uint32_t cpsr;
  uint32_t imsc;
  uint32_t ris;
  uint32_t mis;
  uint32_t icr;
  uint32_t dmacr;
} mil1986be9x_ssp;

typedef struct {
  rtems_libi2c_bus_t bus;
  volatile    mil1986be9x_ssp *regs;
  unsigned clock;
  uint32_t idle_char;
} mil1986be9x_ssp_bus_entry;

static uint32_t mil1986be9x_ssp_trash = 0;

static inline bool mil1986be9x_ssp_is_busy(const mil1986be9x_ssp_bus_entry *bus)
{
  return 0;
}

static void mil1986be9x_ssp_handler(void *arg)
{
  mil1986be9x_ssp_bus_entry *e = (mil1986be9x_ssp_bus_entry *) arg;
  volatile mil1986be9x_ssp *regs = e->regs;
  uint32_t mis = regs->mis;
  uint32_t icr = 0;

  if ((mis & SPI_SSPx_MIS_RORMIS) != 0) {
    /* TODO */
    printk("%s: Receiver overrun!\n", __func__);
    icr |= SPI_SSPx_ICR_RORIC;
  }

  regs->icr = icr;
}

static rtems_status_code mil1986be9x_ssp_init(rtems_libi2c_bus_t *bus)
{
  rtems_status_code sc = RTEMS_SUCCESSFUL;
  rtems_interrupt_level level;
  mil1986be9x_ssp_bus_entry *e = (mil1986be9x_ssp_bus_entry *) bus;
  volatile mil1986be9x_ssp *regs = e->regs;
  unsigned pclk = 48000000UL;
  unsigned pre =
    ((pclk + MIL1986BE9X_SSP_BAUD_RATE - 1) / MIL1986BE9X_SSP_BAUD_RATE + 1) & ~1U;
  rtems_vector_number vector = UINT32_MAX;

  /* Disable module */
  regs->cr1 = 0;

  switch ((uintptr_t) regs) {
    case SPI1_BASE:
      vector = SSP1_IRQn;
      RST_CLK->PER_CLOCK |= RST_CLK_PER_CLOCK_SSP1;
      RST_CLK->SSP_CLOCK |= RST_CLK_SSP_CLOCK_SSP1_CLK_EN |
      				(0 << RST_CLK_SSP_CLOCK_SSP1_BRG_Pos);
      break;
    case SPI2_BASE:
      vector = SSP2_IRQn;
      RST_CLK->PER_CLOCK |= RST_CLK_PER_CLOCK_SSP2;
      RST_CLK->SSP_CLOCK |= RST_CLK_SSP_CLOCK_SSP2_CLK_EN |
      				(0 << RST_CLK_SSP_CLOCK_SSP2_BRG_Pos);
      break;
    default:
      return RTEMS_IO_ERROR;
  }

  /* Set clock select */
  RTEMS_CHECK_SC(sc, "enable module clock: pclk = 48MHz");

  /* Set serial clock rate to save value */
  regs->cr0 = SPI_SSPx_CR0_SCR_Msk;

  /* Set clock prescaler */
  if (pre > 254) {
    pre = 254;
  } else if (pre < 2) {
    pre = 2;
  }
  regs->cpsr = pre;

  /* Save clock value */
  e->clock = pclk / pre;

  /* Enable module and loop back mode */
  regs->cr1 = SPI_SSPx_CR1_LBM | SPI_SSPx_CR1_SSE;

  /* Install interrupt handler */
  sc = rtems_interrupt_handler_install(
    vector,
    "SSP",
    RTEMS_INTERRUPT_UNIQUE,
    mil1986be9x_ssp_handler,
    e
  );
  RTEMS_CHECK_SC(sc, "install interrupt handler");

  /* Enable receiver overrun interrupts */
  e->regs->imsc = SPI_SSPx_IMSC_RORIM;

  return RTEMS_SUCCESSFUL;
}

static rtems_status_code mil1986be9x_ssp_send_start(rtems_libi2c_bus_t *bus)
{
  return RTEMS_SUCCESSFUL;
}

static rtems_status_code mil1986be9x_ssp_send_stop(rtems_libi2c_bus_t *bus)
{
  return RTEMS_SUCCESSFUL;
}

static rtems_status_code mil1986be9x_ssp_send_addr(
  rtems_libi2c_bus_t *bus,
  uint32_t addr,
  int rw
)
{
  mil1986be9x_ssp_bus_entry *e = (mil1986be9x_ssp_bus_entry *) bus;

  if (mil1986be9x_ssp_is_busy(e)) {
    return RTEMS_RESOURCE_IN_USE;
  }

  return RTEMS_SUCCESSFUL;
}

static int mil1986be9x_ssp_set_transfer_mode(
  rtems_libi2c_bus_t *bus,
  const rtems_libi2c_tfr_mode_t *mode
)
{
  mil1986be9x_ssp_bus_entry *e = (mil1986be9x_ssp_bus_entry *) bus;
  volatile mil1986be9x_ssp *regs = e->regs;
  unsigned clk = e->clock;
  unsigned br = mode->baudrate;
  unsigned scr = (clk + br - 1) / br;

  if (mil1986be9x_ssp_is_busy(e)) {
    return -RTEMS_RESOURCE_IN_USE;
  }

  if (mode->bits_per_char != 8) {
    return -RTEMS_INVALID_NUMBER;
  }

  if (mode->lsb_first) {
    return -RTEMS_INVALID_NUMBER;
  }

  if (br == 0) {
    return -RTEMS_INVALID_NUMBER;
  }

  /* Compute new prescaler if necessary */
  if (scr > 256 || scr < 1) {
    unsigned pre = regs->cpsr;
    unsigned pclk = clk * pre;

    while (scr > 256) {
      if (pre > 252) {
        return -RTEMS_INVALID_NUMBER;
      }
      pre += 2;
      clk = pclk / pre;
      scr = (clk + br - 1) / br;
    }

    while (scr < 1) {
      if (pre < 4) {
        return -RTEMS_INVALID_NUMBER;
      }
      pre -= 2;
      clk = pclk / pre;
      scr = (clk + br - 1) / br;
    }

    regs->cpsr = pre;
    e->clock = clk;
  }

  /* Adjust SCR */
  --scr;

  e->idle_char = mode->idle_char;

  while ((regs->sr & SPI_SSPx_SR_TFE) == 0) {
    /* Wait */
  }

  regs->cr0 =  (SPI_SSPx_CR0_DSS_Msk & (0x7 << SPI_SSPx_CR0_DSS_Pos))
    | (SPI_SSPx_CR0_SCR_Msk & (scr << SPI_SSPx_CR0_SCR_Pos))
    | (mode->clock_inv ? SPI_SSPx_CR0_SPO : 0)
    | (mode->clock_phs ? SPI_SSPx_CR0_SPH : 0);

  return 0;
}

static int mil1986be9x_ssp_read_write(
  rtems_libi2c_bus_t *bus,
  unsigned char *in,
  const unsigned char *out,
  int n
)
{
  mil1986be9x_ssp_bus_entry *e = (mil1986be9x_ssp_bus_entry *) bus;
  volatile mil1986be9x_ssp *regs = e->regs;
  int r = 0;
  int w = 0;
  int dr = 1;
  int dw = 1;
  int m = 0;
  uint32_t sr = regs->sr;
  unsigned char trash = 0;
  unsigned char idle_char = (unsigned char) e->idle_char;

  if (mil1986be9x_ssp_is_busy(e)) {
    return -RTEMS_RESOURCE_IN_USE;
  }

  if (n < 0) {
    return -RTEMS_INVALID_SIZE;
  }

  /* Disable DMA on SSP */
  regs->dmacr = 0;

  if (in == NULL) {
    dr = 0;
    in = &trash;
  }

  if (out == NULL) {
    dw = 0;
    out = &idle_char;
  }

  /*
   * Assumption: The transmit and receive FIFOs are empty.  If this assumption
   * is not true an input buffer overflow may occur or we may never exit the
   * loop due to data loss.  This is only possible if entities external to this
   * driver operate on the SSP.
   */

  while (w < n) {
    /* FIFO capacity */
    m = w - r;

    /* Write */
    if ((sr & SPI_SSPx_SR_TNF) != 0 && m < MIL1986BE9X_SSP_FIFO_SIZE) {
      regs->dr = *out;
      ++w;
      out += dw;
    }

    /* Read */
    if ((sr & SPI_SSPx_SR_RNE) != 0) {
      *in = (unsigned char) regs->dr;
      ++r;
      in += dr;
    }

    /* New status */
    sr = regs->sr;
  }

  /* Read outstanding input */
  while (r < n) {
    /* Wait */
    do {
      sr = regs->sr;
    } while ((sr & SPI_SSPx_SR_RNE) == 0);

    /* Read */
    *in = (unsigned char) regs->dr;
    ++r;
    in += dr;
  }

  return n;
}

static int mil1986be9x_ssp_read(rtems_libi2c_bus_t *bus, unsigned char *in, int n)
{
  return mil1986be9x_ssp_read_write(bus, in, NULL, n);
}

static int mil1986be9x_ssp_write(
  rtems_libi2c_bus_t *bus,
  unsigned char *out,
  int n
)
{
  return mil1986be9x_ssp_read_write(bus, NULL, out, n);
}

static int mil1986be9x_ssp_ioctl(rtems_libi2c_bus_t *bus, int cmd, void *arg)
{
  int rv = -1;
  const rtems_libi2c_tfr_mode_t *tm = (const rtems_libi2c_tfr_mode_t *) arg;
  rtems_libi2c_read_write_t *rw = (rtems_libi2c_read_write_t *) arg;

  switch (cmd) {
    case RTEMS_LIBI2C_IOCTL_READ_WRITE:
      rv = mil1986be9x_ssp_read_write(bus, rw->rd_buf, rw->wr_buf, rw->byte_cnt);
      break;
    case RTEMS_LIBI2C_IOCTL_SET_TFRMODE:
      rv = mil1986be9x_ssp_set_transfer_mode(bus, tm);
      break;
    default:
      rv = -RTEMS_NOT_DEFINED;
      break;
  }

  return rv;
}

static const rtems_libi2c_bus_ops_t mil1986be9x_ssp_ops = {
  .init = mil1986be9x_ssp_init,
  .send_start = mil1986be9x_ssp_send_start,
  .send_stop = mil1986be9x_ssp_send_stop,
  .send_addr = mil1986be9x_ssp_send_addr,
  .read_bytes = mil1986be9x_ssp_read,
  .write_bytes = mil1986be9x_ssp_write,
  .ioctl = mil1986be9x_ssp_ioctl
};

static mil1986be9x_ssp_bus_entry mil1986be9x_ssp_bus_table [MIL1986BE9X_SSP_NUMBER] = {
  {
    /* SSP 0 */
    .bus = {
      .ops = &mil1986be9x_ssp_ops,
      .size = sizeof(mil1986be9x_ssp_bus_entry)
    },
    .regs = (volatile mil1986be9x_ssp *) SPI1_BASE,
    .clock = 0,
    .idle_char = 0xffffffff
  }, {
    /* SSP 1 */
    .bus = {
      .ops = &mil1986be9x_ssp_ops,
      .size = sizeof(mil1986be9x_ssp_bus_entry)
    },
    .regs = (volatile mil1986be9x_ssp *) SPI2_BASE,
    .clock = 0,
    .idle_char = 0xffffffff
  }
};

rtems_libi2c_bus_t * const mil1986be9x_ssp_0 =
  (rtems_libi2c_bus_t *) &mil1986be9x_ssp_bus_table [0];

rtems_libi2c_bus_t * const mil1986be9x_ssp_1 =
  (rtems_libi2c_bus_t *) &mil1986be9x_ssp_bus_table [1];
